feat(theme): scoped theming with ThemeScope component#768
feat(theme): scoped theming with ThemeScope component#768paanSinghCoder wants to merge 1 commit intomainfrom
Conversation
Any element with `data-theme` (or wrapped in `<ThemeScope>`) now creates an isolated theme region. Descendants resolve all `--rs-color-*` tokens, `color-scheme`, and the smooth theme-switch transition from the nearest scoped ancestor. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughThe PR introduces a Possibly related issues
Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/www/src/app/examples/scoped-theme/page.tsx`:
- Around line 17-23: The panelStyle object uses a spacing token for borderRadius
(borderRadius: 'var(--rs-space-3)') which teaches the wrong token family; update
the borderRadius value to use the appropriate radius token (e.g., replace
var(--rs-space-3) with the project's radius token such as var(--rs-radius-3) or
the correct --rs-radius-<n>), keeping the rest of panelStyle unchanged so the
example demonstrates the proper radius token usage.
In `@packages/raystack/styles/primitives/appearance.css`:
- Around line 13-20: The CSS unconditionally applies color-scheme when
[data-theme] is present, bypassing ThemeProvider's enableColorScheme flag;
update the rules to only apply when an explicit opt-in marker is present (e.g.,
[data-enable-color-scheme="true"] or a class like .enable-color-scheme) so
ThemeProvider's enableColorScheme prop in ThemeProvider
(packages/raystack/components/theme-provider/theme.tsx) can control whether
native color-scheme is set; ensure the marker is added/removed by ThemeProvider
where enableColorScheme is read so disabling the prop actually prevents the CSS
color-scheme rules from applying.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 68a8df63-d9e9-4b91-beba-bc0d1b81312d
📒 Files selected for processing (13)
apps/www/src/app/examples/scoped-theme/page.tsxapps/www/src/content/docs/theme/overview/index.mdxapps/www/src/content/docs/theme/overview/props.tspackages/raystack/components/theme-provider/__tests__/theme-scope.test.tsxpackages/raystack/components/theme-provider/index.tsxpackages/raystack/components/theme-provider/theme-scope.tsxpackages/raystack/components/theme-provider/theme.tsxpackages/raystack/components/theme-provider/types.tspackages/raystack/index.tsxpackages/raystack/styles/colors.csspackages/raystack/styles/primitives/accent.csspackages/raystack/styles/primitives/appearance.csspackages/raystack/styles/primitives/gray.css
| const panelStyle = { | ||
| minWidth: 320, | ||
| padding: 'var(--rs-space-7)', | ||
| borderRadius: 'var(--rs-space-3)', | ||
| border: '1px solid var(--rs-color-border-base-primary)', | ||
| backgroundColor: 'var(--rs-color-background-base-primary)' | ||
| }; |
There was a problem hiding this comment.
Use a radius token for borderRadius here.
borderRadius is pulling from the spacing scale. That makes the example teach the wrong token family and can drift if spacing and radius scales stop lining up.
Suggested fix
const panelStyle = {
minWidth: 320,
padding: 'var(--rs-space-7)',
- borderRadius: 'var(--rs-space-3)',
+ borderRadius: 'var(--rs-radius-3)',
border: '1px solid var(--rs-color-border-base-primary)',
backgroundColor: 'var(--rs-color-background-base-primary)'
};📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const panelStyle = { | |
| minWidth: 320, | |
| padding: 'var(--rs-space-7)', | |
| borderRadius: 'var(--rs-space-3)', | |
| border: '1px solid var(--rs-color-border-base-primary)', | |
| backgroundColor: 'var(--rs-color-background-base-primary)' | |
| }; | |
| const panelStyle = { | |
| minWidth: 320, | |
| padding: 'var(--rs-space-7)', | |
| borderRadius: 'var(--rs-radius-3)', | |
| border: '1px solid var(--rs-color-border-base-primary)', | |
| backgroundColor: 'var(--rs-color-background-base-primary)' | |
| }; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/www/src/app/examples/scoped-theme/page.tsx` around lines 17 - 23, The
panelStyle object uses a spacing token for borderRadius (borderRadius:
'var(--rs-space-3)') which teaches the wrong token family; update the
borderRadius value to use the appropriate radius token (e.g., replace
var(--rs-space-3) with the project's radius token such as var(--rs-radius-3) or
the correct --rs-radius-<n>), keeping the rest of panelStyle unchanged so the
example demonstrates the proper radius token usage.
| /* Native UI (form controls, scrollbars, text selection) follows the scoped theme */ | ||
| [data-theme="light"] { | ||
| color-scheme: light; | ||
| } | ||
|
|
||
| [data-theme="dark"] { | ||
| color-scheme: dark; | ||
| } |
There was a problem hiding this comment.
enableColorScheme={false} is bypassed by unconditional scoped color-scheme.
ThemeProvider still models enableColorScheme as a conditional behavior (packages/raystack/components/theme-provider/theme.tsx, Line 40 and Lines 90-99 / 258-271), but this CSS now forces native color-scheme whenever data-theme is present. That makes the prop effectively non-functional.
Please gate these rules behind an explicit opt-in attribute/class so disabling enableColorScheme is still respected.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/raystack/styles/primitives/appearance.css` around lines 13 - 20, The
CSS unconditionally applies color-scheme when [data-theme] is present, bypassing
ThemeProvider's enableColorScheme flag; update the rules to only apply when an
explicit opt-in marker is present (e.g., [data-enable-color-scheme="true"] or a
class like .enable-color-scheme) so ThemeProvider's enableColorScheme prop in
ThemeProvider (packages/raystack/components/theme-provider/theme.tsx) can
control whether native color-scheme is set; ensure the marker is added/removed
by ThemeProvider where enableColorScheme is read so disabling the prop actually
prevents the CSS color-scheme rules from applying.
Summary
Screen.Recording.2026-04-30.at.4.15.38.PM.mov
Check usage in
/examples/theme-scopedAny element with
data-theme(or wrapped in<ThemeScope>) creates an isolated theme region. Descendants resolve all--rs-color-*tokens,color-scheme, and the smooth theme-switch transition from the nearest scoped ancestor.The CSS alias chain (
--rs-color-*→--rs-neutral-*→--rs-gray-*) now redeclares each layer on[data-theme="light"]/[data-theme="dark"]so scopes re-anchor cleanly. Adds matchingcolor-schemerules so native UI (form controls, scrollbars) follows the scope.Why a separate
ThemeScope(not extendingThemeProvider)?ThemeProviderbundles global-only concerns: writes todocument.documentElement, localStorage persistence,prefers-color-schemelistener, inline SSR-flash script, and an explicit "nested providers are no-ops" guard. Reusing it for scopes would mean gating every effect on anisScopeflag, removing the nesting guard, and reworking context — same internal split, just behind one export.ThemeScopeis ~30 lines, no state, no effects, does only what scoping needs.Why no
localStorageonThemeScope?Stateless by design. Scopes share no semantic identity, so a single storage key wouldn't fit, and per-scope persistence would reintroduce SSR flash at the scope level (each scope would need its own inline pre-hydration script). Consumers manage state themselves; we deferred a
useScopedThemehelper hook until there's a real ask.Test plan
pnpm testinpackages/raystack(9 new ThemeScope tests pass)/examples/scoped-theme— global and scoped switchers toggle independently; both panels re-theme correctly<div data-theme="dark">inside a light page — native UI (checkboxes, scrollbars) follows the scope🤖 Generated with Claude Code